iT邦幫忙

2023 iThome 鐵人賽

DAY 19
0

延續上一節的內容,這次換成製作開關抽屜的功能,在認識知識點之前,先來完成一個簡單的抽屜。
https://ithelp.ithome.com.tw/upload/images/20230923/201296350ighA4zCPK.png

抽屜基本架構分成在外面的標題 title 、打開抽屜後顯示的內容 content ,以及控制內容是否被打開的布林值 showContent。我們可以將這些資料包成物件裝進陣列中,再用 map 渲染。

標題用 TouchOpacity 來製作,點擊時觸發函式,使 drawerDataList 裡對應的 showContent 被更新。注意,因為 React 更新方式,我們必須使用產生新值的方式,因此必須用 map 來替換掉要更新的值。最後再加上一點樣式,一個最基本的抽屜就完成了。

function Drawer() {
  const [drawerDataList, setDrawerDataList] = useState([
    {
      title: 'A',
      content:
        'content Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent A',
      showContent: false,
    },
    {
      title: 'B',
      content:
        'content Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent B',
      showContent: false,
    },
    {
      title: 'C',
      content:
        'content Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent C',
      showContent: false,
    },
  ]);

  const handlePress = title => {
    const newDrawerDataList = drawerDataList.map(drawerData => {
      if (drawerData.title === title) {
        return {
          ...drawerData,
          showContent: !drawerData.showContent,
        };
      }
      return drawerData;
    });
    setDrawerDataList(newDrawerDataList);
  };

  return (
    <View>
      {drawerDataList.map(drawerData => (
        <View key={drawerData.title}>
          <TouchableOpacity
            onPress={() => handlePress(drawerData.title)}
            style={styles.drawerTitle}>
            <Text>{drawerData.title}</Text>
          </TouchableOpacity>
          {drawerData.showContent && (
            <View style={styles.drawerContainer}>
              <Text>{drawerData.content}</Text>
            </View>
          )}
        </View>
      ))}
    </View>
  );
}

const styles = StyleSheet.create({
  drawerTitle: {
    padding: 10,
    backgroundColor: '#888',
    borderBottomWidth: 1,
  },
  drawerContainer: {
    paddingHorizontal: 8,
    paddingVertical: 10,
  },
});

接著我們可以把這個元件在任何需要的地方引入,也可以依照需求改寫成由外部傳入抽屜資料陣列。不過可能會遇到一個問題,當抽屜上方的內容很多,導致抽屜本身剛好在畫面最下方時,即使點擊了抽屜,抽屜也正常運作,使用者也可能因為看不到抽屜打開誤以為 App 壞了。其實只要手動下滑,就會發現抽屜是有開啟的。

function HomeScreen() {
  return (
    <ScrollView style={styles.container}>
      <Text>
        LoremLoremLoremLorem… 省略
      </Text>
      <Drawer />
    </ScrollView>
  );
const styles = StyleSheet.create({
  container: {
    height: 300,
  },
});

https://ithelp.ithome.com.tw/upload/images/20230923/201296350ymHUu0IXa.png
https://ithelp.ithome.com.tw/upload/images/20230923/20129635abIbT2knIm.png

因此就正式進入今天的主題,要如何讓抽屜元件打開後,可以自動下滑一點呢?首先要認識 onScroll 和 scrollEventThrottle 。

onScroll 這個事件顧名思義是在滑動螢幕時會被觸發,而要使用這個事件,必須搭配 scrollEventThrottle={16} 。因為我們是希望整個畫面下滑,因此將這些相關設定,設在外部元件中:

function HomeScreen() {
  const scrollRef = useRef();

  const handleScroll = () => {
    console.log('scroll');
  };

  return (
    <ScrollView
      ref={scrollRef}
      scrollEventThrottle={16}
      style={styles.container}
      onScroll={handleScroll}>
      <Text>
        LoremLoremLoremLorem… 省略
      </Text>
      <Drawer />
    </ScrollView>
  );
}

因為我們會有很多不同的 drawerTitle ,需要取得點擊的那個一項的位置,才能根據該位置,設定 scrollTo 來下滑。而在 onScroll 的事件裡,只要點擊不同的位置,就能取得不同的 e 值。我們可以在 e.nativeEvent 下找到 contentOffset ,也就是我們所需要的螢幕位置,並且把它儲存到 screenPosition 中。

function HomeScreen() {
  const [screenPosition, setScreenPosition] = useState(0);
  const scrollRef = useRef();

  const handleScroll = e => {
    const {contentOffset} = e.nativeEvent;
    const {y} = contentOffset;
    setScreenPosition(y);
  };
}

有了正確的螢幕位置,就能進一步將此螢幕位置的 y 軸加一點點數字,達到下滑的目的。運用上一節學到的 useRef 和 scrollTo 可以建立一個處理下滑的 scrollDownDrawer 函式。

function HomeScreen() {
  const scrollDownDrawer = () => {
    scrollRef.current?.scrollTo({
      y: screenPosition + 100,
      animated: true,
    });
  };
}

不過,該在什麼時候觸發 scrollDownDrawer 呢?會這麼說是因為,抽屜本來是閉合的,打開後螢幕大小會跟著改變,單純用 scrollTo 似乎無法抵達那些新增區塊的高度。

還好 ScrollView 提供我們一個 props 叫 onContentSizeChange 。當螢幕大小改變時,就會觸發裡頭的內容。我們可以透過他,來呼叫 scrollTo 。

function HomeScreen() {
  return (
    <ScrollView
      ref={scrollRef}
      onContentSizeChange={scrollDownDrawer} // 新增這行
      scrollEventThrottle={16}
      style={styles.container}
      onScroll={handleScroll}>
      … 省略
  )
}

這裡有一個 bug 。由於 onContentSizeChange 會在內容改變高度時觸發,因此剛開始載入頁面時,也會被觸發一次。因此應該改寫 scrollDownDrawer 為:

const scrollDownDrawer = () => {
    if (screenPosition !== 0) {
      // 避免一進入畫面,就直接往下滑動
      scrollRef.current?.scrollTo({
        y: screenPosition + 100,
        animated: true,
      });
    }
};

不過這樣又有另一個問題,就是當使用者完全沒滑動頁面,就直接點擊抽屜時,是無法觸發 scrollDownDrawer 的。對此還有另一種暴力解法,將 screenPosition 和 setScreenPosition 傳到 Drawer 元件中。當點擊標題時若 screenPosition 是 0 ,代表沒有滑動過,則強制把 screenPosition 設為 10 ,這樣 scrollDownDrawer 就能正常運作了。

function HomeScreen() {
  return (
    <Drawer
      screenPosition={screenPosition}
      setScreenPosition={setScreenPosition}
    />
}
function Drawer({screenPosition, setScreenPosition}) {
  const handlePress = title => {
    … 省略
    if (screenPosition === 0) {
      setScreenPosition(10);
    }
  };
}

參考:


上一篇
Day 20. 從實作 onTop 按鈕,認識 useRef 與 scrollTo
下一篇
Day 22. 從製作語言切換功能,認識 Redux
系列文
即使明天老闆突然叫你用 React Native 也可以跟他說好沒問題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言